package ifmo.compiler.task21.lexer;

import ifmo.compiler.task21.lexer.tokens.Token;
import ifmo.compiler.task21.lexer.tokens.TokenFactory;
import ifmo.compiler.task21.lexer.tokens.TokenType;
import ifmo.compiler.task21.lexer.tokens.WrongTokenException;

import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;

/**
 * Automaton for parsing lexes from input stream.
 *
 * @author byaminov
 */
public class LexerAutomaton {

    private InputStream program;

    private enum State {
        BEGIN,
        END,
        LETTERS_READ,
        READ_DIGIT,
        READ_DIGIT_DOT,
        READ_DIGIT_DOT_DIGIT,
        READ_DIGIT_SLASH,
        READ_DIGIT_SLASH_DIGIT,
        SLASH_1_READ,
        SLASH_2_READ,
        R_READ,
        // Symbol states
        EQUAL_READ,
        NOT_READ,
        LESS_READ,
        MORE_READ
    }

    private State state;
    private LinkedList<Character> charBuffer = new LinkedList<Character>();
    private int indexInQueue;

    private int lineIndex=1;
    private int rowIndex=1;

    protected LexerAutomaton(InputStream program) {
        this.program = program;
    }

    protected Token nextToken() throws IOException, WrongTokenException {
        StringBuffer tokenString = new StringBuffer();
        TokenType tokenType = null;
        state = State.BEGIN;
        /* Begin new next token getting: refresh current index in queue */
        indexInQueue = -1;

        while (state != State.END) {
            /* Read a char (from queue) */
            char c = nextChar();

            /* Choose transition based on current state */
            switch (state) {
                case BEGIN:
                    if (isEnd(c)) {
                        consume(tokenString);
                        tokenType = TokenType.END;
                        state = State.END;
                    } else if (isLetter(c)) {
                        consume(tokenString);
                        tokenType = TokenType.IDENTIFIER;
                        state = State.LETTERS_READ;
                    } else if (isDigit(c)) {
                        consume(tokenString);
                        state = State.READ_DIGIT;
                    } else if (isWS(c)) {
                        consume(tokenString);
                        tokenType = TokenType.WS;
                        state = State.END;
                    } else if (c == '/') {
                        consume(tokenString);
                        state = State.SLASH_1_READ;
                    } else
                    // Symbols
                    if (c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c == ']' || c == ':' ||
                            c == ';' || c == ',' || c == '*' || c == '+' || c == '-' || c == '.') {
                        tokenType = TokenType.SYMBOL;
                        consume(tokenString);
                        state = State.END;
                    } else if (c == '=') {
                        consume(tokenString);
                        state = State.EQUAL_READ;
                    } else if (c == '!') {
                        consume(tokenString);
                        state = State.NOT_READ;
                    } else if (c == '<') {
                        consume(tokenString);
                        state = State.LESS_READ;
                    } else if (c == '>') {
                        consume(tokenString);
                        state = State.MORE_READ;
                    } else {
                        throwUnexpectedCharacter(c);
                    }
                    break;
                case LETTERS_READ:
                    if (isLetter(c) || isDigit(c)) {
                        consume(tokenString);
                        // same state
                    } else {
                        // new symbol does not refer to identifier
                        state = State.END;
                    }
                    break;
                case READ_DIGIT:
                    if (isDigit(c)) {
                        consume(tokenString);
                        // same state
                    } else if (c == '.') {
                        consume(tokenString);
                        tokenType = TokenType.FLOATING;
                        state = State.READ_DIGIT_DOT;
                    } else if (c == '/') {
                        consume(tokenString);
                        tokenType = TokenType.RATIONAL;
                        state = State.READ_DIGIT_SLASH;
                    } else {
                        tokenType = TokenType.DECIMAL;
                        state = State.END;
                    }
                    break;
                case READ_DIGIT_DOT:
                    if (isDigit(c)) {
                        consume(tokenString);
                        state = State.READ_DIGIT_DOT_DIGIT;
                    } else {
                        throwUnexpectedCharacter(c);
                    }
                    break;
                case READ_DIGIT_DOT_DIGIT:
                    readRestDigits(tokenString, c);
                    break;
                case READ_DIGIT_SLASH:
                    if (isDigit(c)) {
                        consume(tokenString);
                        state = State.READ_DIGIT_SLASH_DIGIT;
                    } else {
                        throwUnexpectedCharacter(c);
                    }
                    break;
                case READ_DIGIT_SLASH_DIGIT:
                    readRestDigits(tokenString, c);
                    break;
                case SLASH_1_READ:
                    if (c == '/') {
                        tokenType = TokenType.LINE_COMMENT;
                        consume(tokenString);
                        state = State.SLASH_2_READ;
                    } else {
                        tokenType = TokenType.DIVIDE;
                        state = State.END;
                    }
                    break;
                case SLASH_2_READ:
                    if (c == '\r') {
                        consume(tokenString);
                        state = State.R_READ;
                    } else if (c == '\n') {
                        consume(tokenString);
                        state = State.END;
                    } else if (isEnd(c)) {
                        state = State.END;
                    } else {
                        consume(tokenString);
                        // same state
                    }
                    break;
                case R_READ:
                    if (c == '\n') {
                        consume(tokenString);
                        state = State.END;
                    } else if (isEnd(c)) {
                        state = State.END;
                    } else {
                        throwUnexpectedCharacter(c);
                    }
                    break;
                case EQUAL_READ:
                    if (c == '=') {
                        tokenType = TokenType.EQUAL;
                        consume(tokenString);
                    } else {
                        tokenType = TokenType.ASSIGN;
                    }
                    state = State.END;
                    break;
                case NOT_READ:
                    if (c == '=') {
                        tokenType = TokenType.NOT_EQUAL;
                        consume(tokenString);
                    } else {
                        tokenType = TokenType.NOT;
                    }
                    state = State.END;
                    break;
                case LESS_READ:
                    if (c == '=') {
                        tokenType = TokenType.LE;
                        consume(tokenString);
                    } else {
                        tokenType = TokenType.LT;
                    }
                    state = State.END;
                    break;
                case MORE_READ:
                    if (c == '=') {
                        tokenType = TokenType.GE;
                        consume(tokenString);
                    } else {
                        tokenType = TokenType.GT;
                    }
                    state = State.END;
                    break;
                default:
                    assert false : "Unexpected automaton state: " + state;
            }
        }

        assert tokenType != null : "Token type has not been initialized during automaton work";

        return TokenFactory.createToken(tokenType, tokenString.toString());
    }

    private void readRestDigits(StringBuffer tokenString, char c) throws WrongTokenException {
        if (isDigit(c)) {
            consume(tokenString);
            // same state
        } else {
            state = State.END;
        }
    }

    private Character nextChar() throws IOException {
        indexInQueue++;
        if (indexInQueue >= charBuffer.size()) {
            int b = program.read();
            // update char position
            if ((char) b == '\n') {
                lineIndex++;
                rowIndex = 0;
            } else {
                rowIndex++;
            }
            // offer into queue
            charBuffer.offer((char) b);
        }
        return charBuffer.get(indexInQueue);
    }

    private Character consume(StringBuffer buffer) {
        char c = charBuffer.poll();
        indexInQueue = indexInQueue >= 0 ? indexInQueue - 1 : -1;
        if (buffer != null) {
            buffer.append(c);
        }
        return c;
    }

    private void throwUnexpectedCharacter(char c) throws WrongTokenException {
        throw new WrongTokenException("Unexpected character at (" + lineIndex + ":" + rowIndex + "): " + c);
    }

    /* Transition conditions methods */

    private boolean isLetter(char c) {
        return Character.isLetter(c);
    }

    private boolean isDigit(char c) {
        if (!Character.isDigit(c)) {
            return false;
        }
        int d = Character.digit(c, 10);
        return (0 <= d) && (d <= 9);
    }

    private boolean isWS(char c) {
        return c == ' ' || c == '\t' || c == '\r' || c == '\n' || isEnd(c);
    }

    private boolean isEnd(char c) {
        return c == (char) -1;
    }
}
